﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Hosting;
using Microsoft.Practices.ServiceLocation;

namespace Beaker.Injection.Desktop
{
	/// <summary>
	/// The single class that you manually create, after this the rest should be injected.
	/// </summary>
	public sealed class InjectionManager
		: IInjectionManager
	{
		/// <summary>
		/// Search all assemblies (using MEF) in the base directory of the current appdomain and let them register their modules with DI.
		/// </summary>
		/// <param name="skipVerify">Set to true to skip the verify phase.</param>
		/// <returns>The created injection container where all found modules are registerd.</returns>
		[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
		public IQueryContainer SearchAndRegisterModules(bool skipVerify = false)
		{
			string path;

			// when we are running under HttpContext, use the virtual path otherwise the AppDomain path
			if (HostingEnvironment.IsHosted)
				path = Path.Combine(HttpRuntime.AppDomainAppPath, "bin");
			else
				path = AppDomain.CurrentDomain.BaseDirectory;

			return SearchAndRegisterModules(path, skipVerify);
		}

		/// <summary>
		/// Search all assemblies (using MEF) in the given folder and let them register their modules with DI.
		/// </summary>
		/// <param name="folderPath">The folder to search for assemblies.</param>
		/// <param name="skipVerify">Set to true to skip the verify phase.</param>
		/// <returns>The created injection container where all found modules are registerd.</returns>
		[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
		public IQueryContainer SearchAndRegisterModules(string folderPath, bool skipVerify = false)
		{
			return SearchAndRegisterModulesInternal(folderPath, skipVerify);
		}

		/// <summary>
		/// Calls the <see cref="IInitialization.Initialize"/> method of all instances that are registered as <see cref="IInitialization"/> with with <see cref="IContainerBuilder.RegisterAnother{TService,TImplementation}"/>
		/// </summary>
		[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated by Contracts on Interface")]
		public void InitializeModules(IQueryContainer container)
		{
			IEnumerable<IInitialization> initializations = container.GetAllInstances<IInitialization>();

			foreach (IInitialization initialization in initializations)
				initialization.Initialize();
		}

		/// <summary>
		/// Call the <see cref="ICleanup.Cleanup"/> method of all instances that are registered as <see cref="ICleanup"/> with <see cref="IContainerBuilder.RegisterAnother{TService,TImplementation}"/>
		/// </summary>
		[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated by Contracts on Interface")]
		public void CleanupModules(IQueryContainer container)
		{
			IEnumerable<ICleanup> cleanups = container.GetAllInstances<ICleanup>();

			foreach (ICleanup cleanup in cleanups)
				cleanup.Cleanup();
		}

		/// <summary>
		/// Search the given path for assemblies and register any assemblies found using MEF
		/// </summary>
		/// <param name="folderPath">The path to search for assemblies</param>
		/// <param name="skipVerify">Set to true to skip the verify phase.</param>
		/// <returns>The created injection container where all found modules are registerd.</returns>
		private static IQueryContainer SearchAndRegisterModulesInternal(string folderPath, bool skipVerify = false)
		{
			Contract.Requires<ArgumentNullException>(!String.IsNullOrEmpty(folderPath));

			IServiceLocator locator = CreateServiceLocatorThatReturnsModulesAndProviders(folderPath);

			// Find the Injection Provider
			IInjectionProvider injectionProvider = FindInjectionProvider(locator);
			IContainerBuilder builder = injectionProvider.CreateContainerBuilder();

			// Register MEF with the Injection Provider
			injectionProvider.RegisterServiceLocator(builder, locator);

			// now get all modules found.
			IEnumerable<IInjectionModule> injectionModules = locator.GetAllInstances<IInjectionModule>();

			// ask the modules to register themselfs
			foreach (IInjectionModule module in injectionModules)
				module.Register(builder);

			// register local init and cleanup modules so there is always at least one of them
			builder.RegisterAnother<IInitialization, InitializationDummy>();
			builder.RegisterAnother<ICleanup, CleanupDummy>();

			// register the open generic factory
			injectionProvider.RegisterFactory(builder);

			// now build the container
			IQueryContainer queryContainer = injectionProvider.BuildAndLock(builder, skipVerify);

			// setup a service locator with that container
			SetupServiceLocation(queryContainer);

			// and return the build container
			return queryContainer;

		}

		private static void SetupServiceLocation(IQueryContainer queryContainer)
		{
			// now set the service locator to the container, if it was not set by somebody else.
			var locator = new QueryContainerServiceLocatorAdapter(queryContainer);
			if (!ServiceLocator.IsLocationProviderSet)
				ServiceLocator.SetLocatorProvider(() => locator);
		}

		private static IInjectionProvider FindInjectionProvider(IServiceLocator serviceLocator)
		{
			// find a provider using service locator
			List<IInjectionProvider> injectionProviders = serviceLocator.GetAllInstances<IInjectionProvider>().ToList();
			if (injectionProviders.Count == 0)
				throw new InvalidOperationException("No injection provider found. Please add a NuGet package that supplies an injection provider.");
			if (injectionProviders.Count > 1)
			{
				throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Multiple injection providers found. Please remove {0} of these: {1}",
					injectionProviders.Count - 1,
					string.Join(", ", injectionProviders.Select(ip => ip.GetType().FullName))));
			}

			IInjectionProvider injectionProvider = injectionProviders.Single();
			return injectionProvider;
		}

		private static IServiceLocator CreateServiceLocatorThatReturnsModulesAndProviders(string folderPath)
		{
			// MEF1 does not allow finding classes by convention
			// MEF2 requires another extra package to include
			// we simply want to find the modules. 
			// So for now we manually scan assemblies and get the modules

			// interface types to find
			var interfaceTypes = new[] { typeof(IInjectionModule), typeof(IInjectionProvider) };

			// load all assemblies
			IList<Assembly> assemblies = new List<Assembly>();
			string[] dllFiles = Directory.GetFiles(folderPath, "*.dll");

			// scan 
			foreach (string dllFile in dllFiles)
			{
				try
				{
					assemblies.Add(Assembly.LoadFile(dllFile));
				}
				catch (FileLoadException) { }
				catch (FileNotFoundException) { }
				catch (BadImageFormatException) { }
			}

			// for all types in all assemblies, cross join with interface types we search
			// then group by the types that match by the interface they match
			IEnumerable<IGrouping<Type, TypeInfo>> types =
				from assembly in assemblies
				from type in assembly.DefinedTypes
				from interfaceType in interfaceTypes
				where interfaceType.IsAssignableFrom(type)
				where type.IsClass && !type.IsAbstract && !type.IsGenericTypeDefinition
				group type by interfaceType;

			// return the found types as a service locator
			return new DictionaryServiceLocator(types);
		}
	}
}